package ir;

import ic.ast.Visitor;
import ic.ast.decl.ClassType;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclField;
import ic.ast.decl.DeclLibraryMethod;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.DeclStaticMethod;
import ic.ast.decl.DeclVirtualMethod;
import ic.ast.decl.Parameter;
import ic.ast.decl.PrimitiveType;
import ic.ast.decl.PrimitiveType.DataType;
import ic.ast.decl.Program;
import ic.ast.decl.Type;
import ic.ast.expr.BinaryOp;
import ic.ast.expr.Call;
import ic.ast.expr.Expression;
import ic.ast.expr.ExpressionBlock;
import ic.ast.expr.Length;
import ic.ast.expr.Literal;
import ic.ast.expr.NewArray;
import ic.ast.expr.NewInstance;
import ic.ast.expr.Ref;
import ic.ast.expr.RefArrayElement;
import ic.ast.expr.RefField;
import ic.ast.expr.RefVariable;
import ic.ast.expr.StaticCall;
import ic.ast.expr.This;
import ic.ast.expr.UnaryOp;
import ic.ast.expr.VirtualCall;
import ic.ast.stmt.LocalVariable;
import ic.ast.stmt.Statement;
import ic.ast.stmt.StmtAssignment;
import ic.ast.stmt.StmtBlock;
import ic.ast.stmt.StmtBreak;
import ic.ast.stmt.StmtCall;
import ic.ast.stmt.StmtContinue;
import ic.ast.stmt.StmtIf;
import ic.ast.stmt.StmtReturn;
import ic.ast.stmt.StmtWhile;
import ir.TacValueRef.Kind;
import ir.helpers.TacGenerationException;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import pars.Tree;
import scoping.Checker;
import scoping.ScopeTree;
import scoping.blocks.ClassScope;
import scoping.blocks.GlobalScope;
import scoping.blocks.MethodScope;
import scoping.helpers.VariableLocation;

public class TacGenerator implements Visitor {
	
	private final String mainLabel = "_ic_main";
	private int currentRegister = 0;
	private List<DataInstruction> runtimeErrorDataInstructions;
	private List<TacInstruction> runtimeErrorInstructions;
	private Checker checker;
	private List<TacInstruction> globalInstructionList = new ArrayList<TacInstruction>();
	private static List<DataInstruction> globalDataInstructionList =
			new ArrayList<DataInstruction>();

	
	/*
	 * We pass the verifier to use it for type retrieval 
	 */
	public TacGenerator(Checker checker){
		runtimeErrorDataInstructions = new ArrayList<DataInstruction>();
		runtimeErrorInstructions = new ArrayList<TacInstruction>();
		for (RuntimeErrorType errorType:RuntimeErrorType.values()){
			appendErrorHandling(errorType);
		}
		this.checker = checker; 
	}
	
	private int getCurrentRegister(){
		int tmpRegister = currentRegister;
		currentRegister++;
		return tmpRegister;
	}
	
	private void resetRegister() {
		currentRegister = 0;
	}
	
	private TacValueRef getRefArrayElementMemAddress(RefArrayElement location){
		TacValueRef arrayRef=(TacValueRef)location.getArray().accept(this);
		assertArrayInitialized(arrayRef);
		TacValueRef indexRef=(TacValueRef)location.getIndex().accept(this);
		assertArrayIndex(arrayRef, indexRef);			

		TacValueRef realIndexRef;
		if(indexRef.kind.equals(Kind.IMMEDIATE)){
			realIndexRef=new TacValueRef(indexRef.val+1);
		}
		else{
			realIndexRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
			globalInstructionList.add(new TacInstruction("+",
					new TacValueRef[] {indexRef,new TacValueRef(1),realIndexRef}));
		}
		TacValueRef arrayElemRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		globalInstructionList.add(new TacInstruction("+",
				new TacValueRef[] {arrayRef,realIndexRef,arrayElemRef}));
		return arrayElemRef;
	}
	
	private TacValueRef getRefFieldElementMemAddress (Ref location){ // the arg is Ref because field can be either RefField either RefVariable
		ClassScope classScope;
		String fieldName;
		TacValueRef objectRef;
		
		if(location instanceof RefField){ /* written like a.x */
			RefField refField = (RefField) location;
			objectRef = (TacValueRef) refField.getObject().accept(this);
			Type objectType = checker.getType(refField.getObject());
			VariableLocation varLocation =  VariableLocation.getVariableScope(refField.getScope(), objectType.getDisplayName());
			classScope = ((ClassScope)((GlobalScope)varLocation.getScopeTree().getBlock()).getClass(objectType.getDisplayName()).getScope().getBlock());
			fieldName = refField.getField();
			
		
		} else { /* only field name is written (meaning , it is this.x ) */
			RefVariable refVariable = (RefVariable) location;
			objectRef = new TacValueRef(Kind.LOCAL, 0 );
			VariableLocation varLocation = VariableLocation.getVariableScope(refVariable.getScope(), refVariable.getName());
			classScope = (ClassScope)varLocation.getScopeTree().getBlock();
			fieldName = refVariable.getName();
		}
		
		int offset = classScope.getFieldOffset(fieldName);
		TacValueRef fieldRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		globalInstructionList.add(new TacInstruction("+",
				new TacValueRef[] {objectRef, new TacValueRef(offset),fieldRef}));
		return fieldRef;
	}
	

	
	private void appendErrorHandling(RuntimeErrorType errorType) {
		String runtimeErrorLabel = LabelGenerator.generateRuntimeErrorLabel(errorType);
		String dataLabel = runtimeErrorLabel + "_message";
		runtimeErrorDataInstructions.add(new DataInstruction(dataLabel, errorType.toString().length(), errorType.toString()));
		
		TacValueRef labelRef = new TacValueRef(Kind.LABEL, runtimeErrorLabel);
		TacInstruction labelInstruction = new TacInstruction("", new TacValueRef[] { labelRef });
		runtimeErrorInstructions.add(labelInstruction);
		
		TacInstruction dataLabelInstruction = 
				new TacInstruction("param", new TacValueRef[] { 
						new TacValueRef(Kind.LABEL, dataLabel) } );
		TacInstruction printErrorInstuction = 
				new TacInstruction("call", new TacValueRef[] { 
						new TacValueRef(Kind.LABEL, "println") } );
		TacInstruction returnValueInstruction = 
				new TacInstruction("param", new TacValueRef[] { 
						new TacValueRef(1) } );
		TacInstruction callExitInstruction = 
				new TacInstruction("call", new TacValueRef[] {
						new TacValueRef(Kind.LABEL, "exit") } );
		runtimeErrorInstructions.add(dataLabelInstruction);
		runtimeErrorInstructions.add(printErrorInstuction);
		runtimeErrorInstructions.add(returnValueInstruction);
		runtimeErrorInstructions.add(callExitInstruction);
	}
	
	private void assertArrayInitialized(TacValueRef arrayRef){
		TacValueRef targetRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
		TacInstruction isArrayInitInstruction = 
				new TacInstruction("<", new TacValueRef[] { arrayRef, new TacValueRef(0), targetRef } );
		globalInstructionList.add(isArrayInitInstruction);
		TacValueRef arrayNullErrorLabel = new TacValueRef(Kind.LABEL, 
				LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.NullPointerDereferrence));
		TacInstruction arrayNullInstruction = 
				new TacInstruction("if", new TacValueRef[]{targetRef, arrayNullErrorLabel } );
		globalInstructionList.add(arrayNullInstruction);
	}
	
	private void assertArrayIndex(TacValueRef arrayRef, TacValueRef indexRef){
		// Check index > 0
		TacValueRef answerRef = null;
		TacValueRef indexOutOfBoundsLabel = 
				new TacValueRef(Kind.LABEL, 
						LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.ArrayIndexOutOfBounds));
		if (indexRef.kind.equals(Kind.IMMEDIATE)){
			if (indexRef.val < 0){
				globalInstructionList.add(new TacInstruction("goto", new TacValueRef[]{indexOutOfBoundsLabel}));
			}
		} else {
			answerRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			TacInstruction checkIndexNegativeInstruction = 
					new TacInstruction("<", new TacValueRef[]{indexRef, new TacValueRef(0), answerRef});
			globalInstructionList.add(checkIndexNegativeInstruction);
			globalInstructionList.add(
					new TacInstruction("if", new TacValueRef[]{
						answerRef,
						indexOutOfBoundsLabel
					}));
		}
		
		// Check index < length
		TacValueRef lengthRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
		TacInstruction getArrayLength = new TacInstruction("[]",
				new TacValueRef[]{arrayRef, lengthRef});
		globalInstructionList.add(getArrayLength);
		TacValueRef isLegalRef = answerRef != null ? answerRef : new TacValueRef(Kind.LOCAL, getCurrentRegister());
		TacInstruction checkIndexLegal = 
				new TacInstruction(">", new TacValueRef[]{lengthRef, indexRef, isLegalRef}); 
		globalInstructionList.add(checkIndexLegal);
		globalInstructionList.add(
				new TacInstruction("if!", new TacValueRef[]{
					isLegalRef,
					indexOutOfBoundsLabel
				}));
	}
	
	private void assertArraySizePositive(TacValueRef sizeRef){
		TacValueRef arraySizeNegativeErrorLabel = new TacValueRef(Kind.LABEL, 
				LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.ArrayAllocationNegative));
		if (sizeRef.kind.equals(Kind.IMMEDIATE)) {
			if (sizeRef.val < 1) {
				globalInstructionList.add(new TacInstruction("goto", new TacValueRef[]{arraySizeNegativeErrorLabel}));
			}
		} else {
			TacValueRef isArraySizeNonPositiveRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			TacInstruction checkArrayNegativeInstruction = 
					new TacInstruction("<", new TacValueRef[] { sizeRef, new TacValueRef(1), isArraySizeNonPositiveRef } );
			globalInstructionList.add(checkArrayNegativeInstruction);
			TacInstruction arraySizeNegativeInstruction = 
					new TacInstruction("if", new TacValueRef[]{isArraySizeNonPositiveRef, arraySizeNegativeErrorLabel } );
			globalInstructionList.add(arraySizeNegativeInstruction);
		}
	}
	
	public String invoke(Tree ast) throws TacGenerationException {
		StringBuilder sb = new StringBuilder();
		if (!(ast.root instanceof Program))
		{
			throw new TacGenerationException("Root ast node is not Program");
		}
		Program p = (Program)ast.root;
		p.accept(this);
		
		for (TacInstruction ti:globalInstructionList){
			System.out.println(ti.toString());
			sb.append(ti.toString() + "\n");
		}
		for (TacInstruction ti:runtimeErrorInstructions){
			System.out.println(ti.toString());
			sb.append(ti.toString() + "\n");
		}
		
		System.out.println(".data");
		sb.append(".data");
		for (DeclClass declClass:p.getClasses()){
			DVInstruction dvi = new DVInstruction(declClass);
			System.out.println(dvi);
			sb.append(dvi);
		}
		for(DataInstruction di:globalDataInstructionList){
			System.out.println(di.toString());
			sb.append(di.toString()+"\n");
		}
		for(DataInstruction di:runtimeErrorDataInstructions){
			System.out.println(di.toString());
			sb.append(di.toString()+"\n");
		}
		
		return sb.toString().trim();
	}
	
	
	
	@Override
	public Object visit(Program program) {
		globalInstructionList.add(new TacInstruction("goto", 
				new TacValueRef[]{new TacValueRef(Kind.LABEL, mainLabel)}));
		for(DeclClass declClass:program.getClasses()){
			declClass.accept(this);
		}
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(DeclClass icClass) {
		LabelGenerator.setCurrentClass(icClass.getName());
		for (DeclMethod method:icClass.getMethods()){
			method.accept(this);
		}
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(DeclField field) {
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(DeclVirtualMethod method) {
		/* register 0 is for "this"! */
		resetRegister();
		currentRegister++; /* start registers from 1 */
		for(Parameter param:method.getFormals()){
			TacValueRef paramRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			param.getScope().getBlock().AddVariable(param.getName(), paramRef.val);
		}
		LabelGenerator.setCurrentMethod(method.getName());
		String methodLabel=LabelGenerator.generateMethodName();
		globalInstructionList.add(new TacInstruction("", 
				new TacValueRef[]{ 
					new TacValueRef(Kind.LABEL, methodLabel)}));
		
		for (Statement s:method.getStatements()){
			s.accept(this);
		}
		
		if(method.getType().getDisplayName().equals(DataType.VOID.toString())){
			globalInstructionList.add(new TacInstruction("ret"));
		}
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(DeclStaticMethod method) {
		if (method.getScope() == null || !(method.getScope().getParent().getBlock() instanceof ClassScope)){
			return TacValueRef.createErrornousTacValueRef("Method has no scope or is not defined in class");
		}
		resetRegister();
		for (Parameter param:method.getFormals()){
			TacValueRef paramRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			param.getScope().getBlock().AddVariable(param.getName(), paramRef.val);
		}
		LabelGenerator.setCurrentMethod(method.getName());
		String methodLabel = null;
		if (method.getName().equals("main")){
			methodLabel = mainLabel;
		} else {
			methodLabel = LabelGenerator.generateMethodName();
		}
		globalInstructionList.add(new TacInstruction("", 
									new TacValueRef[]{ 
										new TacValueRef(Kind.LABEL, methodLabel)}));
		for (Statement s:method.getStatements()){
			s.accept(this);
		}
		if (method.getName().equals("main")){
			globalInstructionList.add(new TacInstruction("param", new TacValueRef[]{new TacValueRef(Kind.IMMEDIATE, 0)}));
			globalInstructionList.add(new TacInstruction("call", new TacValueRef[]{new TacValueRef(Kind.LABEL, "exit")}));
		}
		if(method.getType().getDisplayName().equals(DataType.VOID.toString())){
			globalInstructionList.add(new TacInstruction("ret"));
		}
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(DeclLibraryMethod method) {
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(Parameter formal) {
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(PrimitiveType type) {
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(ClassType type) {
		return TacValueRef.getEmpty();	
	}

	@Override
	public Object visit(StmtAssignment assignment) {
		TacValueRef rhsRef=(TacValueRef)assignment.getAssignment().accept(this);

		if(assignment.getVariable() instanceof RefVariable){ 
			RefVariable location = (RefVariable)assignment.getVariable();
			VariableLocation varLocation = 
					VariableLocation.getVariableScope(location.getScope(), location.getName());
			if (varLocation.getScopeTree().getBlock() instanceof ClassScope){ /* field ! */
				
				TacValueRef fieldAddr = 
						getRefFieldElementMemAddress(location);
				
				globalInstructionList.add(new TacInstruction("[]=",
						new TacValueRef[] {fieldAddr, rhsRef}));
			}
			else { /* regular variable */
				TacValueRef lhsRef=(TacValueRef)assignment.getVariable().accept(this);
				globalInstructionList.add(new TacInstruction("=",
						new TacValueRef[] {rhsRef,lhsRef}));
			}
		}
		else if (assignment.getVariable() instanceof RefArrayElement){
			TacValueRef arrayElementAddr = 
					getRefArrayElementMemAddress((RefArrayElement)assignment.getVariable());
			
			// Set value on element address
			globalInstructionList.add(new TacInstruction("[]=",
					new TacValueRef[] {arrayElementAddr, rhsRef}));
		}
		else if (assignment.getVariable() instanceof RefField){
			TacValueRef fieldAddr = 
					getRefFieldElementMemAddress((RefField)assignment.getVariable());
			
			globalInstructionList.add(new TacInstruction("[]=",
					new TacValueRef[] {fieldAddr, rhsRef}));
		}
		
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtCall callStatement) {
		return callStatement.getCall().accept(this);
	}
	
	@Override
	public Object visit(StmtReturn returnStatement) 
	{
		TacInstruction retInstruction;
		if (returnStatement.hasValue())
		{
			TacValueRef retVal = (TacValueRef) returnStatement.getValue().accept(this);
			retInstruction = new TacInstruction("ret",retVal);
		}else{//no return value
			retInstruction = new TacInstruction("ret");
		}
		globalInstructionList.add(retInstruction);
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtIf ifStatement) {
		TacValueRef if_value_ref=new TacValueRef(Kind.LABEL,LabelGenerator.generateIfLabel());
		TacValueRef else_value_ref=new TacValueRef(Kind.LABEL,LabelGenerator.generateElseLabel());
		TacValueRef end_if_value_ref=new TacValueRef(Kind.LABEL,LabelGenerator.generateEndIfLabel());
	
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {if_value_ref}));
		TacValueRef conditionRef=(TacValueRef)ifStatement.getCondition().accept(this);
		globalInstructionList.add(new TacInstruction("if!",new TacValueRef[] {conditionRef,else_value_ref} ));
		ifStatement.getOperation().accept(this);
		globalInstructionList.add(new TacInstruction("goto",new TacValueRef[] {end_if_value_ref}  ));
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {else_value_ref} ));
		if(ifStatement.getElseOperation()!=null){
			ifStatement.getElseOperation().accept(this);
		}
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {end_if_value_ref}));
		
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtWhile whileStatement) {
		/*Remark: we want re-eval the condition each time */
		TacValueRef while_value_ref=new TacValueRef(Kind.LABEL,LabelGenerator.generateWhileLabel());
		TacValueRef end_while_value_ref=new TacValueRef(Kind.LABEL,LabelGenerator.generateEndWhileLabel());
		
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {while_value_ref}));
		TacValueRef conditionRef=(TacValueRef)whileStatement.getCondition().accept(this);
		globalInstructionList.add(new TacInstruction("if!",new TacValueRef[] {conditionRef,end_while_value_ref})); /* the only way to exit the loop */
		whileStatement.getOperation().accept(this);
		globalInstructionList.add(new TacInstruction("goto",new TacValueRef[] {while_value_ref}));
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {end_while_value_ref}));
		
		LabelGenerator.exitCurrentLoop();
		
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtBreak breakStatement) {
		globalInstructionList.add(new TacInstruction("goto",new TacValueRef[] {new TacValueRef(Kind.LABEL,LabelGenerator.generateBreakLabel()) }));
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtContinue continueStatement) {
		globalInstructionList.add(new TacInstruction("goto",new TacValueRef[] {new TacValueRef(Kind.LABEL,LabelGenerator.generateContinueLabel()) } ));
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(StmtBlock statementsBlock) {
		for(Statement stmt:statementsBlock.getStatements()){
			stmt.accept(this);
		}
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(LocalVariable localVariable) {
		TacValueRef sourceRef = new TacValueRef(0);
		TacValueRef targetRef;
		if (localVariable.isInitialized() && (localVariable.getType() instanceof PrimitiveType)){
			targetRef = localVariable.getType().getArrayDimension()>=1 ? 
					(TacValueRef)localVariable.getInitialValue().accept(this) :
						new TacValueRef(Kind.LOCAL, getCurrentRegister());
			if (localVariable.getType().getArrayDimension() == 0 && localVariable.getType() instanceof PrimitiveType){		
				Expression initValue = localVariable.getInitialValue();
				sourceRef = (TacValueRef)initValue.accept(this);
				globalInstructionList.add(new TacInstruction("=", 
						new TacValueRef[] { sourceRef, targetRef }));
			}
				
		
		} else if (localVariable.isInitialized() && (localVariable.getType() instanceof ClassType)){
			
			targetRef = (TacValueRef)localVariable.getInitialValue().accept(this);

						
		} else	{ /* not initialized */ 
			if (localVariable.getType().getArrayDimension() >= 1 ||
					localVariable.getType().getDisplayName().equals(DataType.STRING.toString()) || 
					localVariable.getType() instanceof ClassType) {
				sourceRef = new TacValueRef(-1); /* init value is null */
			}
			targetRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			globalInstructionList.add(new TacInstruction("=", 
					new TacValueRef[] { sourceRef, targetRef }));
			
		}
		localVariable.getScope().getBlock().AddVariable(localVariable.getName(), targetRef.val);
		return TacValueRef.getEmpty();
	}

	@Override
	public Object visit(RefVariable location) {
		VariableLocation varLocation = 
				VariableLocation.getVariableScope(location.getScope(), location.getName());
		if(varLocation.getScopeTree().getBlock() instanceof ClassScope){ /* field */
			return getFieldValRef(location);
		}
		int register = varLocation.getRegister();
		if (register < 0){
			TacValueRef arrayNullErrorLabel = new TacValueRef(Kind.LABEL, 
					LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.NullPointerDereferrence));
			globalInstructionList.add(new TacInstruction("goto",new TacValueRef[] {arrayNullErrorLabel}));
			return new TacValueRef(-1);
			
		} else 	{
			if (varLocation.getType().getDisplayName().equals(DataType.STRING.toString()) && 
				varLocation.getType().getArrayDimension() == 0){
				TacValueRef stringAddress = new TacValueRef(Kind.LOCAL, register);
				assertArrayInitialized(stringAddress);
			}
			return new TacValueRef(Kind.LOCAL, register);
		}
	}

	private TacValueRef getFieldValRef(Ref location) {
		TacValueRef fieldMemRef = getRefFieldElementMemAddress(location);
		TacValueRef fieldValRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
		globalInstructionList.add(new TacInstruction("[]",
				new TacValueRef[] {fieldMemRef, fieldValRef}));
		return fieldValRef;
	}

	@Override
	public Object visit(RefField location) {
		
		return getFieldValRef(location);	
		
	}

	@Override
	public Object visit(RefArrayElement location) {
		TacValueRef arrayElemRef = getRefArrayElementMemAddress(location);
		TacValueRef arrayElemVal=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		globalInstructionList.add(new TacInstruction("[]",
				new TacValueRef[] {arrayElemRef,arrayElemVal}));
		return arrayElemVal;
	}

	@Override
	public Object visit(StaticCall call) {
		processStaticActualParams(call);
		VariableLocation varLocation = VariableLocation.getVariableScope(call.getScope(), call.getClassName());
		DeclClass methodClass = ((GlobalScope)varLocation.getScopeTree().getBlock()).getClass(call.getClassName());
		DeclMethod method = VariableLocation.getClassMethod(methodClass, call.getMethod());
	
		String labelName = call.getClassName().equals("Library") 
				? call.getMethod()				
				: LabelGenerator.generate(methodClass.getName(), method.getName()); 

		return appendReturnTac(method.getType(), labelName); 	
	}

	@Override
	public Object visit(VirtualCall call) {
		ClassScope classScope;
		if (call.hasExplicitObject())
		{
			Type objectType = checker.getType(call.getObject());
			VariableLocation varLocation =  VariableLocation.getVariableScope(call.getScope(), objectType.getDisplayName());
			classScope = ((ClassScope)((GlobalScope)varLocation.getScopeTree().getBlock()).getClass(objectType.getDisplayName()).getScope().getBlock());
			
		} else 
		{
			VariableLocation varLocation = VariableLocation.getVariableScope(call.getScope(), call.getMethod());
			classScope = (ClassScope)varLocation.getScopeTree().getBlock();
		}
		
		DeclMethod method = classScope.getMethod(call.getMethod());
		if (method instanceof DeclStaticMethod){ /* calling a static method */
			processStaticActualParams(call);
			String labelName = LabelGenerator.generate(LabelGenerator.getCurrentClass(),call.getMethod());
			return appendReturnTac(method.getType(), labelName);
		}
		else { /* calling a virtual method */
			int offset = classScope.getMethodOffset(call.getMethod());
			
			TacValueRef methodClassInstancePointer = call.hasExplicitObject() ? (TacValueRef)call.getObject().accept(this) : new TacValueRef(Kind.LOCAL, 0);
			
			TacValueRef dispatchVectorPointer = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			globalInstructionList.add (new TacInstruction("[]",
					new TacValueRef[] {methodClassInstancePointer, dispatchVectorPointer}));
			
			TacValueRef methodAddressPointer = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			globalInstructionList.add (new TacInstruction ("+",
					new TacValueRef[] {dispatchVectorPointer,new TacValueRef(offset), methodAddressPointer}));
			
			TacValueRef methodPointer = new TacValueRef (Kind.LOCAL,getCurrentRegister());
			globalInstructionList.add(new TacInstruction("[]",
					new TacValueRef[] {methodAddressPointer, methodPointer }));
			processActualParams(call);
			if (method.getType() instanceof PrimitiveType &&
					((PrimitiveType)method.getType()).getDisplayName().equals(DataType.VOID.toString())){
				globalInstructionList.add(new TacInstruction("call", 
						new TacValueRef[] {methodPointer}));
			} else {
				TacValueRef targetRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
				globalInstructionList.add(new TacInstruction("call", 
						new TacValueRef[] { methodPointer, targetRef}));
				return targetRef;
			}
			return TacValueRef.getEmpty();	
		}	   
	}

	private void processStaticActualParams(Call call){
		List<TacValueRef> paramRefs = new ArrayList<TacValueRef>();
		
		for(Expression arg:call.getArguments()){ //prepare arguments
			paramRefs.add((TacValueRef) arg.accept(this));
		}
		for (TacValueRef paramRef:paramRefs){
			globalInstructionList.add(new TacInstruction("param", new TacValueRef[] {paramRef}));
		}
	}
	private void processActualParams(VirtualCall call){
		if(call.hasExplicitObject()){
			TacValueRef objectRef =(TacValueRef) call.getObject().accept(this);
			globalInstructionList.add(new TacInstruction("param",new TacValueRef[] {objectRef}));
		}
		else {
			globalInstructionList.add(new TacInstruction("param",new TacValueRef[] {new TacValueRef(Kind.LOCAL,0)}));
		}
		processStaticActualParams(call);
	}
	
	private TacValueRef appendReturnTac(Type methodReturnType, String labelName){
		if (methodReturnType instanceof PrimitiveType &&
				((PrimitiveType)methodReturnType).getDisplayName().equals(DataType.VOID.toString())){
			globalInstructionList.add(new TacInstruction("call", 
					new TacValueRef[] { new TacValueRef(Kind.LABEL, labelName) }));
		} else {
			TacValueRef targetRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
			globalInstructionList.add(new TacInstruction("call", 
					new TacValueRef[] { new TacValueRef(Kind.LABEL, labelName),
										targetRef}));
			return targetRef;
		}
		return TacValueRef.getEmpty();
	}
	
	@Override
	public Object visit(This thisExpression) {
		return new TacValueRef(Kind.LOCAL,0);
	}

	@Override
	public Object visit(NewInstance newClass) {
		VariableLocation varLocation = VariableLocation.getVariableScope(newClass.getScope(), newClass.getName());
		ClassScope classScope = ((ClassScope)((GlobalScope)varLocation.getScopeTree().getBlock()).getClass(newClass.getName()).getScope().getBlock());
		
		 int numberOfFields = classScope.getClassSize();
		 String virtualTypeName = newClass.getName(); 
		 
		 globalInstructionList.add(new TacInstruction("param",
				 new TacValueRef[] {new TacValueRef(numberOfFields)}));
		 TacValueRef newInstanceRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
		 globalInstructionList.add(new TacInstruction("call",
				 new TacValueRef[] {new TacValueRef(Kind.LABEL,"alloc"), newInstanceRef}));
		 TacValueRef dispatchVectorRef = new TacValueRef(Kind.LABEL, LabelGenerator.generateClassDVLabel(virtualTypeName)); 
		 globalInstructionList.add(new TacInstruction("[]=",
				 new TacValueRef[] {newInstanceRef, dispatchVectorRef})); 
		 return newInstanceRef;
		 	
	}

	@Override
	public Object visit(NewArray newArray) { // TODO : array of objects , i think there's nothing to do here... 
		TacValueRef arraySizeValueRef=(TacValueRef)newArray.getSize().accept(this);
		
		assertArraySizePositive(arraySizeValueRef);
		TacValueRef arrReg;
		if(arraySizeValueRef.kind.equals(Kind.IMMEDIATE)){
			TacValueRef arraySizePlusOne=new TacValueRef(arraySizeValueRef.val+1);
			globalInstructionList.add(new TacInstruction("param",
					new TacValueRef[] {arraySizePlusOne}));
			arrReg=new TacValueRef(Kind.LOCAL,getCurrentRegister());
			globalInstructionList.add(new TacInstruction("call",
					new TacValueRef[] {new TacValueRef(Kind.LABEL,"alloc"),arrReg } ));
			globalInstructionList.add(new TacInstruction("[]=",
					new TacValueRef[] {arrReg, arraySizeValueRef} ));
			return arrReg;
			
		} 
		TacValueRef arraySizePlusOne=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		arrReg=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		globalInstructionList.add(new TacInstruction("+",
				new TacValueRef[] {arraySizeValueRef,new TacValueRef(1),arraySizePlusOne}));
		globalInstructionList.add(new TacInstruction("param",
				new TacValueRef[] {arraySizePlusOne}));
		globalInstructionList.add(new TacInstruction("call",
				new TacValueRef[] {new TacValueRef(Kind.LABEL,"alloc"),arrReg } ));	
		
		TacValueRef arraySizeValueRefCopy=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		
		globalInstructionList.add(new TacInstruction("=",
				new TacValueRef[] {arraySizeValueRef,arraySizeValueRefCopy} ));
		
		globalInstructionList.add(new TacInstruction("[]=",
				new TacValueRef[] {arrReg, arraySizeValueRefCopy}));
		
		
		
		return arrReg;
		
		
	}

	@Override
	public Object visit(Length length) {
		TacValueRef arrayRef = (TacValueRef) length.getArray().accept(this);
		assertArrayInitialized(arrayRef);
		TacValueRef len = new TacValueRef(Kind.LOCAL,getCurrentRegister());
		TacInstruction instruction = new TacInstruction("[]", new TacValueRef[] {arrayRef ,len });
		globalInstructionList.add(instruction);
		return len;	
	}

	@Override
	public Object visit(Literal literal) {
		switch(literal.getType().toString()){
			case "int":
				return new TacValueRef(Integer.parseInt(literal.getValue().toString()));
			case "string":
				String strLabel = LabelGenerator.generateStringLiteralLabel((String)literal.getValue());
				DataInstruction dataInstruction = new DataInstruction(strLabel, literal.getValue().toString().length(), literal.getValue().toString()); 
				globalDataInstructionList.add(dataInstruction);
				return new TacValueRef(Kind.LABEL, strLabel);
			case "boolean":
				return new TacValueRef(Boolean.parseBoolean(literal.getValue().toString()) ? 1 : 0);
			case "void":
				return new TacValueRef(-1);
			default:
				return TacValueRef.createErrornousTacValueRef(String.format("Unknown literal type %s", literal.getType()));
		}
	}

	@Override
	public Object visit(UnaryOp unaryOp) {
		TacValueRef operandRef=(TacValueRef)unaryOp.getOperand().accept(this);
		TacValueRef negRef=null;
		
		if(unaryOp.getOperator().toString().equals("!")){
			if(operandRef.kind.equals(Kind.IMMEDIATE)){
				if(operandRef.val > 0){
					return new TacValueRef(0);
				} else{
					return new TacValueRef(1);
				}
			} else {
				negRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
				globalInstructionList.add(new TacInstruction("!",
						operandRef,negRef));
				return negRef;
			}
		} else {
			if(operandRef.kind.equals(Kind.IMMEDIATE)){
				return new TacValueRef(-1*operandRef.val);
			}
			else{
				negRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
				globalInstructionList.add(new TacInstruction("*",
						new TacValueRef[] {operandRef,new TacValueRef(-1),negRef}));
				return negRef;
			}
			
		}

	}

	@Override
	public Object visit(BinaryOp binaryOp) {
		if(binaryOp.getOperator().toString().equals("&&") || binaryOp.getOperator().toString().equals("||")) {
			return visitAndOrUsingShortCircuiting(binaryOp.getFirstOperand(),binaryOp.getSecondOperand(),binaryOp.getOperator().toString());
		}
		TacValueRef leftOperand=(TacValueRef)binaryOp.getFirstOperand().accept(this);
		TacValueRef rightOperand=(TacValueRef)binaryOp.getSecondOperand().accept(this);
		TacValueRef returnRef=null;
		Type lhsType = checker.getType(binaryOp.getFirstOperand());
		Type rhsType = checker.getType(binaryOp.getSecondOperand());
		int bool;
		boolean areBothImmediate=leftOperand.kind.equals(Kind.IMMEDIATE) &&
								 rightOperand.kind.equals(Kind.IMMEDIATE);
		if(!areBothImmediate){
			returnRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		}
		
		switch (binaryOp.getOperator().toString())
		{
			case "+":
				PrimitiveType lhsPrimType = (PrimitiveType)lhsType;
				if (lhsPrimType.getDisplayName().equals(DataType.VOID.toString()) || 
						((PrimitiveType)rhsType).getDisplayName().equals(DataType.VOID.toString())){
					// if null + null
						TacValueRef arrayNullErrorLabel = new TacValueRef(Kind.LABEL, 
								LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.NullPointerDereferrence));
						TacInstruction nullInstruction = 
								new TacInstruction("goto", new TacValueRef[]{ arrayNullErrorLabel } );
						globalInstructionList.add(nullInstruction);
						return new TacValueRef(-1); // Error 
				}
				if (lhsPrimType.getDisplayName().equals(DataType.INT.toString())){
					if (areBothImmediate){
						return new TacValueRef(leftOperand.val + rightOperand.val);
					} else {
						TacValueRef[] tacValueRefs = { leftOperand, rightOperand, returnRef };
						TacInstruction tacInstruction = new TacInstruction("+", tacValueRefs);
						globalInstructionList.add(tacInstruction);
					}
				} else if (lhsPrimType.getDisplayName().equals(DataType.STRING.toString())){
					TacInstruction firstParam = new TacInstruction("param", new TacValueRef[]{leftOperand});
					globalInstructionList.add(firstParam);
					TacInstruction secondParam = new TacInstruction("param", new TacValueRef[]{rightOperand});
					globalInstructionList.add(secondParam);
					TacValueRef methodName = new TacValueRef(Kind.LABEL, "stringCat");
					TacInstruction stringCatCall = new TacInstruction("call", 
							new TacValueRef[] { methodName, returnRef });
					globalInstructionList.add(stringCatCall);
				} else {
					return TacValueRef.createErrornousTacValueRef(String.format("Invalid type for binary operation + (%s)", lhsType.getDisplayName()));
				}
				break;
			case "-":
				if(areBothImmediate){
					return new TacValueRef(leftOperand.val-rightOperand.val);
				}
				else{
					globalInstructionList.add(new TacInstruction("-",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "*":
				if(areBothImmediate){
					return new TacValueRef(leftOperand.val*rightOperand.val);
				}
				else{
					globalInstructionList.add(new TacInstruction("*",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "/":
				TacValueRef runtimeErrorLabel = 
					new TacValueRef(Kind.LABEL, LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.DivisionByZero));
				if(areBothImmediate){
					if (rightOperand.val == 0){
						globalInstructionList.add(new TacInstruction("goto", new TacValueRef[]{runtimeErrorLabel}));
						return new TacValueRef(-1); /*invalid program */
					}
					return new TacValueRef(leftOperand.val/rightOperand.val);
				}
				else{
					TacValueRef answerRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
					globalInstructionList.add(new TacInstruction("==", new TacValueRef[]{rightOperand, new TacValueRef(0), answerRef}));
					globalInstructionList.add(new TacInstruction("if", new TacValueRef[]{answerRef, runtimeErrorLabel}));					
					globalInstructionList.add(new TacInstruction("/",  new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "%":
				runtimeErrorLabel = 
					new TacValueRef(Kind.LABEL, LabelGenerator.generateRuntimeErrorLabel(RuntimeErrorType.DivisionByZero));
				if(areBothImmediate){
					if (rightOperand.val == 0){
						globalInstructionList.add(new TacInstruction("goto", new TacValueRef[]{runtimeErrorLabel}));
						return new TacValueRef(-1); /*invalid program */
					}
					return new TacValueRef(leftOperand.val%rightOperand.val);
				}
				else{
					TacValueRef answerRef = new TacValueRef(Kind.LOCAL, getCurrentRegister());
					globalInstructionList.add(new TacInstruction("==", new TacValueRef[]{rightOperand, new TacValueRef(0), answerRef}));
					globalInstructionList.add(new TacInstruction("if", new TacValueRef[]{answerRef, runtimeErrorLabel}));					
					globalInstructionList.add(new TacInstruction("%",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "<":
				if(areBothImmediate){
					bool=leftOperand.val<rightOperand.val ? 1:0;
					return new TacValueRef(bool);
				}
				else{
					globalInstructionList.add(new TacInstruction("<",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "<=":
				if(areBothImmediate){
					bool=leftOperand.val<=rightOperand.val ? 1:0;
					return new TacValueRef(bool);
				}
				else{
					globalInstructionList.add(new TacInstruction("<=",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case ">":
				if(areBothImmediate){
					bool=leftOperand.val>rightOperand.val ? 1:0;
					return new TacValueRef(bool);
				}
				else{
					globalInstructionList.add(new TacInstruction(">",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case ">=":
				if(areBothImmediate){
					bool=leftOperand.val>=rightOperand.val ? 1:0;
					return new TacValueRef(bool);
				}
				else{
					globalInstructionList.add(new TacInstruction(">=",
							new TacValueRef[] {leftOperand,rightOperand,returnRef} ));
					break;
				}
			case "==":
			case "!=":
				int boolTrueValue = binaryOp.getOperator().toString().equals("==") ? 1 : 0;
                int boolFalseValue = binaryOp.getOperator().toString().equals("!=") ? 0 : 1;
                TacValueRef targetRefLhs = leftOperand;
                TacValueRef targetRefRhs = rightOperand;
                if (lhsType instanceof PrimitiveType){
                    if (areBothImmediate){
                        bool = leftOperand.val == rightOperand.val ? boolTrueValue : boolFalseValue;
                        return new TacValueRef(bool);
                    }
                }
                TacInstruction equalsTacInstruction = new TacInstruction(binaryOp.getOperator().toString(), new TacValueRef[] {targetRefLhs, targetRefRhs, returnRef});
                globalInstructionList.add(equalsTacInstruction);
                break;
		}
		
		return returnRef;
	}
	
	private Object visitAndOrUsingShortCircuiting(Expression firstOperand, Expression secondOperand, String opString) {
		TacValueRef lhsOperand=(TacValueRef)firstOperand.accept(this);
		TacValueRef rhsOperand;
		
		if (lhsOperand.kind.equals(Kind.IMMEDIATE)){
			if ((opString.equals("&&") && lhsOperand.val == 0) || (opString.equals("||") && lhsOperand.val >0 ) ){
				return lhsOperand;
			}
			else { /* the first doesn't effect the computation */
				return secondOperand.accept(this);
			}
		}
		
		TacValueRef startRef=new TacValueRef(Kind.LABEL,LabelGenerator.generateShortCircuitingLabel());
		TacValueRef endRef=new TacValueRef(Kind.LABEL,LabelGenerator.generateEndShortCircuitingLabel_Label());
		TacValueRef resultRef=new TacValueRef(Kind.LOCAL,getCurrentRegister());
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {startRef}));
		globalInstructionList.add(new TacInstruction("=",new TacValueRef[] {lhsOperand,resultRef}));
		if(opString.equals("&&")){
			globalInstructionList.add(new TacInstruction("if!",new TacValueRef[]{lhsOperand,endRef}));
		}
		else if (opString.equals("||")) {
			globalInstructionList.add(new TacInstruction("if",new TacValueRef[]{lhsOperand,endRef}));
		}
		rhsOperand=(TacValueRef)secondOperand.accept(this);
		globalInstructionList.add(new TacInstruction("=",new TacValueRef[] {rhsOperand,resultRef}));
		globalInstructionList.add(new TacInstruction("",new TacValueRef[] {endRef} ));
		return resultRef;
	}

	@Override
	public Object visit(ExpressionBlock expressionBlock) {
		return TacValueRef.getEmpty();
	}
}
